import isMergeableObjectUtil from "./isMergeableObjectUtil"

function emptyTarget(val: any) {
  return Array.isArray(val) ? [] : {}
}

function cloneUnlessOtherwiseSpecified(value: any, options: deepmerge.OptionsObj) {
  return (options.clone !== false && options.isMergeableObject(value))
    ? deepmerge(emptyTarget(value), value, options)
    : value
}

function defaultArrayMerge(target: any[], source: any[], options: deepmerge.OptionsObj) {
  return target.concat(source).map(function (element) {
    return cloneUnlessOtherwiseSpecified(element, options)
  })
}

function getMergeFunction(key: string, options: deepmerge.OptionsObj) {
  if (!options.customMerge) {
    return deepmerge
  }
  var customMerge = options.customMerge(key)
  return typeof customMerge === 'function' ? customMerge : deepmerge
}

function getEnumerableOwnPropertySymbols(target: any[]): any {
  return Object.getOwnPropertySymbols
    ? Object.getOwnPropertySymbols(target).filter(function (symbol) {
      return target.propertyIsEnumerable(symbol)
    })
    : []
}

function getKeys(target: any[]): any {
  return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target))
}

function propertyIsOnObject(object: any[], property: string) {
  try {
    return property in object
  } catch (_) {
    return false
  }
}
// Protects from prototype poisoning and unexpected merging up the prototype chain.
function propertyIsUnsafe(target: any[], key: string) {
  return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet,
    && !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain,
      && Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable.
}
interface obj {
  [key: string | number]: string | number | Function
}
function mergeObject(target: any[], source: any[], options: deepmerge.OptionsObj) {
  const destination: obj = {}
  if (options.isMergeableObject(target)) {
    getKeys(target).forEach(function (key: any) {
      destination[key] = cloneUnlessOtherwiseSpecified(target[key], options)
    })
  }
  getKeys(source).forEach(function (key: any) {
    if (propertyIsUnsafe(target, key)) {
      return
    }
    if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) {
      destination[key] = getMergeFunction(key, options)(target[key], source[key], options)
    } else {
      destination[key] = cloneUnlessOtherwiseSpecified(source[key], options)
    }
  })
  return destination
}

function deepmerge<T>(x: Partial<T>, y: Partial<T>, options?: deepmerge.OptionsObj): T;
function deepmerge<T1, T2>(x: Partial<T1>, y: Partial<T2>, options?: deepmerge.OptionsObj): T1 & T2;
function deepmerge(x: any, y: any, options?: deepmerge.OptionsObj): any {
  options = options || {}
  options.arrayMerge = options.arrayMerge || defaultArrayMerge
  options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified
  options.isMergeableObject = options.isMergeableObject || isMergeableObjectUtil
  var sourceIsArray = Array.isArray(y)
  var targetIsArray = Array.isArray(x)
  var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray

  if (!sourceAndTargetTypesMatch) {
    return cloneUnlessOtherwiseSpecified(y, options)
  } else if (sourceIsArray) {
    return options.arrayMerge(x, y, options)
  } else {
    return mergeObject(x, y, options)
  }
}

function isObject(o: any) {
  return Object.prototype.toString.call(o) === '[object Object]';
}

namespace deepmerge {
  export interface OptionsObj {
    arrayMerge?(target: any[], source: any[], options?: OptionsObj): any[];
    clone?: boolean;
    customMerge?: any; //(key: string, options?: OptionsObj) => ((x: any, y: any) => any) | undefined
    cloneUnlessOtherwiseSpecified?: any;
    isMergeableObject?: any
  }
  export function all(objects: object[], options?: OptionsObj): object;
  export function all<T>(objects: Partial<T>[], options?: OptionsObj): T;
  export function all<T>(objects: any[], options?: OptionsObj): any {
    if (!Array.isArray(objects)) {
      throw new Error('first argument should be an array')
    }
    return objects.reduce(function (prev, next) {
      return deepmerge(prev, next, options)
    }, {})
  }
  //判断是否是真实地object对象
  export function isPlainObject(o: any) {
    let ctor, prot;
    if (isObject(o) === false) return false;
    ctor = o.constructor;
    if (ctor === undefined) return true;
    prot = ctor.prototype;
    if (isObject(prot) === false) return false;
    if (prot.hasOwnProperty('isPrototypeOf') === false) {
      return false;
    }
    return true;
  }
}

export default deepmerge
